#include <string.h>

#include <iostream>
#include <cstring>
#include <cassert>
#include <vector>
#include <string>

#include "texture.h"

#define STB_IMAGE_IMPLEMENTATION

struct header {
    unsigned char       identifier[12];
    unsigned int        endianness;
    unsigned int        glType;
    unsigned int        glTypeSize;
    unsigned int        glFormat;
    unsigned int        glInternalFormat;
    unsigned int        glBaseInternalFormat;
    unsigned int        pixelWidth;
    unsigned int        pixelHeight;
    unsigned int        pixelDepth;
    unsigned int        arrayElements;
    unsigned int        faces;
    unsigned int        mipLevels;
    unsigned int        keyPairBytes;
};

union keyValuePair {
    unsigned int        size;
    unsigned char       rawBytes[4];
};


static const unsigned char identifier[] = {
    0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A
};

static const unsigned int swap32(const unsigned int u32)
{
    union {
        unsigned int u32;
        unsigned char u8[4];
    } a, b;

    a.u32 = u32;
    b.u8[0] = a.u8[3];
    b.u8[1] = a.u8[2];
    b.u8[2] = a.u8[1];
    b.u8[3] = a.u8[0];

    return b.u32;
}

static unsigned int calculateStride(const header &h, unsigned int width, unsigned int pad = 4)
{
    unsigned int channels = 0;

    switch (h.glBaseInternalFormat) {
        case GL_RED:
            channels = 1;
            break;
        case GL_RG:
            channels = 2;
            break;
        case GL_RGB:
            channels = 3;
            break;
        case GL_RGBA:
            channels = 4;
            break;
    }

    unsigned int stride = h.glTypeSize * channels * width;

    stride = (stride + (pad - 1)) & ~(pad - 1);

    return stride;
}

static unsigned int calculateFaceSize(const header &h)
{
    unsigned int stride = calculateStride(h, h.pixelWidth);

    return stride * h.pixelHeight;
}

GLuint Texture::loadKtxImage(const std::string &image)
{
    FILE *fp = nullptr;
    header h;
    size_t dataStart, dataEnd;
    GLenum target = GL_NONE;

    fp = fopen(image.data(), "rb");

    if (!fp)
        return 0;

    if (fread(&h, sizeof(h), 1, fp) != 1)
        return 0;

    if (memcmp(h.identifier, identifier, sizeof(identifier)) != 0)
        return 0;

    if (h.endianness == 0x04030201) {
        // No swap needed
    } else if (h.endianness == 0x01020304) {
        // Swap needed
        h.endianness            = swap32(h.endianness);
        h.glType                = swap32(h.glType);
        h.glTypeSize            = swap32(h.glTypeSize);
        h.glFormat              = swap32(h.glFormat);
        h.glInternalFormat      = swap32(h.glInternalFormat);
        h.glBaseInternalFormat  = swap32(h.glBaseInternalFormat);
        h.pixelWidth            = swap32(h.pixelWidth);
        h.pixelHeight           = swap32(h.pixelHeight);
        h.pixelDepth            = swap32(h.pixelDepth);
        h.arrayElements         = swap32(h.arrayElements);
        h.faces                 = swap32(h.faces);
        h.mipLevels             = swap32(h.mipLevels);
        h.keyPairBytes          = swap32(h.keyPairBytes);
    } else {
        return 0;
    }

    // Guess target (texture type)
    if (h.pixelHeight == 0) {
#ifdef USE_GLES
        /* For GLES, there is no GL_TEXTURE_1D. Force to use TEXTURE_2D with height = 1 */
        h.pixelHeight = 1;

        if (h.arrayElements == 0) {
            target = GL_TEXTURE_2D;
        }
        else {
            target = GL_TEXTURE_2D_ARRAY;
        }
#else
        if (h.arrayElements == 0) {
            target = GL_TEXTURE_1D;
        } else {
            target = GL_TEXTURE_1D_ARRAY;
        }
#endif
    } else if (h.pixelDepth == 0) {
        if (h.arrayElements == 0) {
            if (h.faces == 0) {
                target = GL_TEXTURE_2D;
            } else {
                target = GL_TEXTURE_CUBE_MAP;
            }
        } else {
            if (h.faces == 0) {
                target = GL_TEXTURE_2D_ARRAY;
            } else {
                target = GL_TEXTURE_CUBE_MAP_ARRAY;
            }
        }
    } else {
        target = GL_TEXTURE_3D;
    }

    // Check for insanity...
    if (target == GL_NONE ||                                    // Couldn't figure out target
            (h.pixelWidth == 0) ||                                  // Texture has no width???
            (h.pixelHeight == 0 && h.pixelDepth != 0)) {            // Texture has depth but no height???
        return 0;
    }

    dataStart = ftell(fp) + h.keyPairBytes;
    fseek(fp, 0, SEEK_END);
    dataEnd = ftell(fp);
    fseek(fp, dataStart, SEEK_SET);

    auto data = new unsigned char[dataEnd - dataStart];
    memset(data, 0, dataEnd - dataStart);
    fread(data, 1, dataEnd - dataStart, fp);
    fclose(fp);

    if (h.mipLevels == 0)
        h.mipLevels = 1;

#ifdef USE_GLES
    /* GL_R16 indicates that the format has 16 bits in red, but does not specify
     * whether the format is a 16-bit integer, unsigned integer, or floating point.
     * GLES requires internal format to specify whether to use GL_R16I, GL_R16UI, or GL_16F.
     * In GCBS tests, the texture is sampled in shader as GL_R16UI. So we specify it here */
    if (h.glInternalFormat == GL_R16 && h.glFormat == GL_RED) {
        h.glInternalFormat = GL_R16UI;
        h.glFormat = GL_RED_INTEGER;
    }

    /* glTexImage2D in OpenGL supports both GL_BGR and GL_RGB.
     * However, glTexImage2D in OpenGL ES only supports GL_RGB.
     * Forcibly converting glFormat from GL_BGR to GL_RGB results in red and blue inversion,
     * but the performance should not be affected. */
    if (h.glFormat == GL_BGR) {
        h.glFormat = GL_RGB;
    }
#endif

    GLuint tex = 0;
    glGenTextures(1, &tex);
    glBindTexture(target, tex);

    switch (target) {
#ifndef USE_GLES
        case GL_TEXTURE_1D:
            glTexStorage1D(GL_TEXTURE_1D, h.mipLevels, h.glInternalFormat, h.pixelWidth);
            glTexSubImage1D(GL_TEXTURE_1D, 0, 0, h.pixelWidth, h.glFormat, h.glInternalFormat, data);
            break;
#endif
        case GL_TEXTURE_2D:
            if (h.glType == GL_NONE) {
                glCompressedTexImage2D(GL_TEXTURE_2D, 0, h.glInternalFormat, h.pixelWidth, h.pixelHeight, 0,
                                       420 * 380 / 2, data);
            } else {
                glTexStorage2D(GL_TEXTURE_2D, h.mipLevels, h.glInternalFormat, h.pixelWidth, h.pixelHeight);

                unsigned char *ptr = data;
                unsigned int height = h.pixelHeight;
                unsigned int width = h.pixelWidth;
                glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
                for (unsigned int i = 0; i < h.mipLevels; i++) {
                    //glTexImage2D(GL_TEXTURE_2D, i, h.glInternalFormat, width, height, 0, h.glFormat, h.glType, ptr);
                    glTexSubImage2D(GL_TEXTURE_2D, i, 0, 0, width, height, h.glFormat, h.glType, ptr);
                    ptr += height * calculateStride(h, width, 1);
                    height >>= 1;
                    width >>= 1;
                    if (!height)
                        height = 1;
                    if (!width)
                        width = 1;
                }

            }
            break;
        case GL_TEXTURE_3D:
            glTexStorage3D(GL_TEXTURE_3D, h.mipLevels, h.glInternalFormat, h.pixelWidth, h.pixelHeight,
                           h.pixelDepth);
            glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, h.pixelWidth, h.pixelHeight, h.pixelDepth, h.glFormat,
                            h.glType, data);
            break;
#ifndef USE_GLES
        case GL_TEXTURE_1D_ARRAY:
            glTexStorage2D(GL_TEXTURE_1D_ARRAY, h.mipLevels, h.glInternalFormat, h.pixelWidth, h.arrayElements);
            glTexSubImage2D(GL_TEXTURE_1D_ARRAY, 0, 0, 0, h.pixelWidth, h.arrayElements, h.glFormat, h.glType,
                            data);
            break;
#endif
        case GL_TEXTURE_2D_ARRAY:
            glTexStorage3D(GL_TEXTURE_2D_ARRAY, h.mipLevels, h.glInternalFormat, h.pixelWidth, h.pixelHeight,
                           h.arrayElements);
            glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, h.pixelWidth, h.pixelHeight, h.arrayElements,
                            h.glFormat, h.glType, data);
            break;
        case GL_TEXTURE_CUBE_MAP:
            glTexStorage2D(GL_TEXTURE_CUBE_MAP, h.mipLevels, h.glInternalFormat, h.pixelWidth, h.pixelHeight);
            // glTexSubImage3D(GL_TEXTURE_CUBE_MAP, 0, 0, 0, 0, h.pixelwidth, h.pixelheight, h.faces, h.glformat, h.gltype, data);
            {
                unsigned int faceSize = calculateFaceSize(h);
                for (unsigned int i = 0; i < h.faces; i++) {
                    glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, 0, 0, h.pixelWidth, h.pixelHeight,
                                    h.glFormat, h.glType, data + faceSize * i);
                }
            }
            break;
        case GL_TEXTURE_CUBE_MAP_ARRAY:
            glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, h.mipLevels, h.glInternalFormat, h.pixelWidth,
                           h.pixelHeight, h.arrayElements);
            glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, h.pixelWidth, h.pixelHeight,
                            h.faces * h.arrayElements, h.glFormat, h.glType, data);
            break;
        default:                                               // Should never happen
            assert(1 && "bad image target");
    }

    if (h.mipLevels == 1)
        glGenerateMipmap(target);

    delete [] data;


    return tex;
}

Texture::Texture(const std::string &file)
{
    textureID = 0;
    width = 0;
    height = 0;
    bitDepth = 0;
    fileLocation = file;
}

bool Texture::loadTexture()
{
    unsigned char *data = stbi_load(fileLocation.data(), &width, &height, &bitDepth, 0);
    if (!data) {
        std::cout << "failed to find: " << fileLocation << "\n";
        return false;
    }

    if (bitDepth > 4 || bitDepth < 3) {
        return false;
    }

    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    int format = bitDepth == 3 ? GL_RGB : GL_RGBA;

    glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);

    glBindTexture(GL_TEXTURE_2D, 0);

    stbi_image_free(data);

    return true;
}

void Texture::useTexture()
{
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, textureID);
}

void Texture::clearTexture()
{
    glDeleteTextures(1, &textureID);
    textureID = 0;
    width = 0;
    height = 0;
    bitDepth = 0;
    fileLocation = "";
}

bool Texture::loadCubeMap(const std::array<std::string, 6> &files, GLuint *texture)
{
    glGenTextures(1, texture);
    glBindTexture(GL_TEXTURE_CUBE_MAP, *texture);

    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_REPEAT);

    for (int i = 0; i < 6; i++) {
        int width, height, bpp;
        unsigned char *data = stbi_load(files[i].data(), &width, &height, &bpp, 0);
        if (!data) {
            return false;
        }

        GLenum format = bpp == 3 ? GL_RGB : GL_RGBA;
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, format, width, height, 0,
                     format, GL_UNSIGNED_BYTE, data);
        stbi_image_free(data);
    }
    glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
    return true;
}

Texture::~Texture()
{
    clearTexture();
}
